iT邦幫忙

2026 iThome 鐵人賽

DAY 17
0
Software Development

Kotlin 手刻 Ktor 從零開始系列 第 17

Kotlin 手刻 Ktor 從零開始 Day 17 內建 Middleware,CORS

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20260822/20121948NIqCtgaIUJ.png

CORS (Cross-Origin Resource Sharing) 是瀏覽器的安全機制,只要你的 API 會被前端網頁呼叫,就會遇到的問題

  • 開發時前端跑在 http://localhost:5173
  • API 跑在 http://localhost:8080

同樣是 localhost,但 port 不同就是不同 origin,server 沒有正確回應 CORS headers 的話,瀏覽器就會擋,前端的 fetch() 只會拿到一個模糊的 network error

這篇會做出 CorsMiddleware,處理 preflight request (OPTIONS),並讓 CORS 設定可以讓使用者調整

什麼是 Origin ? 什麼時候會 preflight ?

Origin 由三個東西組成,scheme (http/https)、host、port,只要其中一個不同,就是不同 origin

http://localhost:5173  ← 前端
http://localhost:8080  ← API

scheme 一樣、host 一樣,但 port 不同,不同 origin

瀏覽器在以下情況會先發一個 OPTIONS request (稱為 preflight),問 server 是否允許接下來的真正 request

  • 使用非簡單 method (PUT、DELETE、PATCH)
  • 帶非簡單 header (例如 Authorization、自訂的 X- header)
  • Content-Type 不是 application/x-www-form-urlencodedmultipart/form-datatext/plain

現在前端呼叫 API 的方式,幾乎都會踩到上面的條件,因為大多數 API 用 JSON (Content-Type: application/json),再加上 Authorization header,所以幾乎每一支跨域 request 前面都會多一次 preflight

CorsConfig 用 data class 做設定

CORS middleware 需要知道哪些 origin、method、header 是被允許的,用 data class 把這些設定集中管理

data class CorsConfig(
    val allowedOrigins: List<String> = listOf("*"),
    val allowedMethods: List<String> = listOf("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"),
    val allowedHeaders: List<String> = listOf("Content-Type", "Authorization"),
    val maxAge: Int = 86400,
)

maxAge 是 preflight response 的快取秒數,設 86400 (一天) 可以減少 preflight 次數

allowedOrigins 預設 * 代表接受所有 origin

Kotlin 的 data class 天生支援 copy(),使用者可以基於預設值微調

val config = CorsConfig(
    allowedOrigins = listOf("http://localhost:5173", "https://myapp.com"),
    allowedHeaders = listOf("Content-Type", "Authorization", "X-Request-Id"),
)

先補一個 header 查詢的入口

在寫 middleware 之前,得先還第 04 篇留下來的一筆債

toRelixRequest() 是用 requestHeaders.toMap() 把 JDK 的 Headers 轉成普通的 Map,而 JDK 會把 header name 正規化成「首字大寫、其餘全小寫」的形式,Headers 自己查詢時不分大小寫,但一旦 toMap() 變成普通的 Map,那個能力也跟著不見了

Access-Control-Request-Method  ← client 送出去的
Access-control-request-method  ← 進到 map 之後的樣子

CORS 剛好是第一個會遇到這件事的地方,Access-Control-Request-Method 這種多段的 header name,用原本的大小寫去查 map 一定是 nullOrigin 反而是最容易讓人以為沒有問題的那個,因為它只有一個單字,正規化前後長得一樣,應該都查得到值

所以先在 RelixRequest 上補一個統一的查詢入口

data class RelixRequest(
    val method: String,
    val path: String,
    val headers: Map<String, List<String>> = emptyMap(),
    val queryParameters: Map<String, List<String>> = emptyMap(),
    val body: ByteArray = ByteArray(0),
) {
    fun header(name: String): String? = headers.entries
        .firstOrNull { (key) -> key.equals(name, ignoreCase = true) }
        ?.value
        ?.firstOrNull()
}

第 04 篇那個 header keys are normalized by JDK 測試,記錄的正是這個 helper 要負責處理的行為,現在它變成 header() 的規格來源

之後第 19 篇讀 body 要拿 Content-Type、第 20 篇 Content Negotiation 要拿 Accept、第 23 篇認證要拿 Authorization,全部走這個入口,不再直接查 map

TDD 先寫測試

測試要涵蓋六種情境,正常 request 加 CORS header、preflight 回 204、不允許的 origin、wildcard 放行、JDK 正規化過的 header key 也要認得出 preflight、沒有 Origin header 的 request

import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
import kotlin.test.assertTrue

class CorsMiddlewareTest {

    @Test
    fun `adds CORS headers to normal request`() {
        val config = CorsConfig(allowedOrigins = listOf("http://localhost:5173"))
        val app = RelixApplication()
        app.use(corsMiddleware(config))
        app.routing {
            get("/api/data") { ok("hello") }
        }

        val testKit = RelixTestKit(app)
        val response = testKit.handleRequest(
            method = "GET",
            path = "/api/data",
            headers = mapOf("Origin" to listOf("http://localhost:5173")),
        )

        assertEquals(200, response.statusCode)
        assertEquals(
            listOf("http://localhost:5173"),
            response.headers["Access-Control-Allow-Origin"],
        )
    }

    @Test
    fun `preflight returns 204 with correct headers`() {
        val config = CorsConfig(allowedOrigins = listOf("http://localhost:5173"))
        val app = RelixApplication()
        app.use(corsMiddleware(config))
        app.routing {
            post("/api/data") { created("done") }
        }

        val testKit = RelixTestKit(app)
        val response = testKit.handleRequest(
            method = "OPTIONS",
            path = "/api/data",
            headers = mapOf(
                "Origin" to listOf("http://localhost:5173"),
                "Access-Control-Request-Method" to listOf("POST"),
            ),
        )

        assertEquals(204, response.statusCode)
        assertEquals(0, response.body.size)  // 204 沒有 body
        assertEquals(
            listOf("http://localhost:5173"),
            response.headers["Access-Control-Allow-Origin"],
        )
        assertTrue(
            response.headers["Access-Control-Allow-Methods"]
                ?.first()?.contains("POST") == true,
        )
    }

    @Test
    fun `disallowed origin gets no CORS headers`() {
        val config = CorsConfig(allowedOrigins = listOf("http://localhost:5173"))
        val app = RelixApplication()
        app.use(corsMiddleware(config))
        app.routing {
            get("/api/data") { ok("hello") }
        }

        val testKit = RelixTestKit(app)
        val response = testKit.handleRequest(
            method = "GET",
            path = "/api/data",
            headers = mapOf("Origin" to listOf("http://evil.com")),
        )

        assertEquals(200, response.statusCode)
        assertNull(response.headers["Access-Control-Allow-Origin"])
        // 沒有 ACAO,但仍要有 Vary,否則共享快取會誤用
        assertEquals(listOf("Origin"), response.headers["Vary"])
    }

    @Test
    fun `wildcard origin allows any origin`() {
        val config = CorsConfig(allowedOrigins = listOf("*"))
        val app = RelixApplication()
        app.use(corsMiddleware(config))
        app.routing {
            get("/api/data") { ok("hello") }
        }

        val testKit = RelixTestKit(app)
        val response = testKit.handleRequest(
            method = "GET",
            path = "/api/data",
            headers = mapOf("Origin" to listOf("http://anything.com")),
        )

        assertEquals(listOf("*"), response.headers["Access-Control-Allow-Origin"])
    }

    @Test
    fun `preflight is detected with JDK normalized header key`() {
        val config = CorsConfig(allowedOrigins = listOf("http://localhost:5173"))
        val app = RelixApplication()
        app.use(corsMiddleware(config))
        app.routing {
            post("/api/data") { created("done") }
        }

        val testKit = RelixTestKit(app)
        val response = testKit.handleRequest(
            method = "OPTIONS",
            path = "/api/data",
            headers = mapOf(
                "Origin" to listOf("http://localhost:5173"),
                // JDK adapter 交給我們的就是這個大小寫
                "Access-control-request-method" to listOf("POST"),
            ),
        )

        assertEquals(204, response.statusCode)
    }

    @Test
    fun `request without Origin header passes through normally`() {
        val config = CorsConfig(allowedOrigins = listOf("http://localhost:5173"))
        val app = RelixApplication()
        app.use(corsMiddleware(config))
        app.routing {
            get("/api/data") { ok("hello") }
        }

        val testKit = RelixTestKit(app)
        val response = testKit.handleRequest("GET", "/api/data")

        assertEquals(200, response.statusCode)
        assertNull(response.headers["Access-Control-Allow-Origin"])
    }
}

實作 CorsMiddleware

middleware 的邏輯分兩步,判斷是不是 preflight,是的話短路回 204,不是的話正常走 pipeline 再補 header

fun corsMiddleware(config: CorsConfig = CorsConfig()): RelixMiddleware = { next ->
    val origin = request.header("Origin")

    if (origin == null) {
        // 沒有 Origin header,不是跨域 request,直接放行
        next()
    } else if (!isOriginAllowed(origin, config)) {
        // Origin 不在白名單,不加 CORS header
        // 但「有沒有 CORS header」本身就取決於 Origin,還是要宣告 Vary
        next().header("Vary", "Origin")
    } else if (isPreflight(request)) {
        // Preflight request,短路回 204
        preflightResponse(origin, config)
    } else {
        // 正常跨域 request,補 CORS header
        val response = next()
        val allowedOrigin = if ("*" in config.allowedOrigins) "*" else origin
        val corsResponse = response.header("Access-Control-Allow-Origin", allowedOrigin)
        if (allowedOrigin == "*") corsResponse else corsResponse.header("Vary", "Origin")
    }
}

private fun isOriginAllowed(origin: String, config: CorsConfig): Boolean {
    return "*" in config.allowedOrigins || origin in config.allowedOrigins
}

private fun isPreflight(request: RelixRequest): Boolean {
    return request.method == "OPTIONS"
        && request.header("Access-Control-Request-Method") != null
}

private fun preflightResponse(origin: String, config: CorsConfig): RelixResponse {
    val allowedOrigin = if ("*" in config.allowedOrigins) "*" else origin
    val headers = mutableMapOf(
        "Access-Control-Allow-Origin" to listOf(allowedOrigin),
        "Access-Control-Allow-Methods" to listOf(config.allowedMethods.joinToString(", ")),
        "Access-Control-Allow-Headers" to listOf(config.allowedHeaders.joinToString(", ")),
        "Access-Control-Max-Age" to listOf(config.maxAge.toString()),
    )
    if (allowedOrigin != "*") headers["Vary"] = listOf("Origin")
    return RelixResponse(204, headers, ByteArray(0))
}

只要回應會依 request 的 Origin 改變,就要加 Vary: Origin,避免共享快取把某個 origin 的 CORS 回應誤用到另一個 origin

其餘邏輯拆成三個 private function

  • isOriginAllowed,檢查 origin 有沒有在白名單裡,如果 allowedOrigins 包含 *,任何 origin 都放行。也因為這樣,「不在白名單」那條分支只有白名單模式跑得到,不需要再判斷一次 wildcard
  • isPreflight,method 是 OPTIONS 而且有 Access-Control-Request-Method header,查詢走 header(),不受 JDK 正規化影響
  • preflightResponse,回 204,帶齊四個 CORS header。它不比對 request 的 Access-Control-Request-Headers,直接回設定裡的清單,瀏覽器會自己判斷夠不夠

白名單模式會回實際 origin 並加 Vary: Origin,wildcard 模式則回 *。以後要支援 credentials,就不能再讓 wildcard 過

每個情境的 response headers

正常跨域 GET

Request

GET /api/data HTTP/1.1
Origin: http://localhost:5173

Response

HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://localhost:5173
Vary: Origin
Content-Type: text/plain

Preflight (POST + Authorization)

Request

OPTIONS /api/data HTTP/1.1
Origin: http://localhost:5173
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Authorization, Content-Type

Response

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://localhost:5173
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, PATCH, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
Vary: Origin

204 沒有 body。瀏覽器收到這些 header 之後,就會發出真正的 POST request

不允許的 origin

Request

GET /api/data HTTP/1.1
Origin: http://evil.com

Response

HTTP/1.1 200 OK
Vary: Origin
Content-Type: text/plain

沒有 Access-Control-Allow-Origin header。瀏覽器看到之後會擋住 response,前端的 fetch() 拿到 error

Vary: Origin 還是要加,這個回應「有沒有 CORS header」本身就取決於 Origin,少了 Vary,共享快取可能把這份沒有 Access-Control-Allow-Origin 的回應,餵給下一個來自白名單 origin 的請求

Wildcard 與 Credentials 的衝突

Access-Control-Allow-Origin: * 最方便,但有限制,如果前端 request 帶了 credentials: "include" (例如要送 cookie),瀏覽器規定 server 不能回 *,必須回具體的 origin

# ✗ 瀏覽器會拒絕
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

# ✓ 必須回具體 origin
Access-Control-Allow-Origin: http://localhost:5173
Access-Control-Allow-Credentials: true

這裡先不處理 credentials,如果你之後要支援,改法很單純,把 wildcard 改成 config 裡明確列出的 origin,再加上 Access-Control-Allow-Credentials: true

瀏覽器禁止 wildcard 搭配 credentials,是為了避免任何網站都能讀取帶有使用者憑證的跨來源回應

還有一件事要分清楚,CORS 控制的是瀏覽器要不要把 response 交給前端程式,它不能取代 CSRF token、SameSite cookie 或 Origin 驗證

Access-Control-Allow-Origin 也不能在一個回應裡列多個 origin,server 要先比對白名單,再回這次 request 的那一個

安裝順序

CORS middleware 要裝在最外層 (跟 logging 同層或更外面),因為 preflight request 需要被攔住,不能讓它跑進 auth 之類的 middleware

val app = RelixApplication()
app.use(loggingMiddleware())
app.use(corsMiddleware(config))
app.use(errorHandlingMiddleware())
app.use(authMiddleware)

authMiddleware 要到第 23 篇才會做,這裡只是示意順序,如果 CORS 裝在 auth 後面,preflight OPTIONS 會被 auth 擋掉 (因為 preflight 不帶 token),前端就會收到 401 而不是正確的 CORS response

在 main 裡組起來跑一次

把設定跟 middleware 裝進 main,用 curl 把前面三個情境各打一次

fun main() {
    val config = CorsConfig(
        allowedOrigins = listOf("http://localhost:5173"),
    )

    val app = RelixApplication()
    app.use(loggingMiddleware())
    app.use(corsMiddleware(config))

    app.routing {
        get("/api/data") { ok("data") }
        post("/api/data") { created("saved") }
    }

    JdkHttpServerAdapter(app).start(8080)
}

允許的 origin

curl -i -H "Origin: http://localhost:5173" localhost:8080/api/data
HTTP/1.1 200 OK
Content-type: text/plain; charset=utf-8
Access-control-allow-origin: http://localhost:5173
Vary: Origin
Content-length: 4

data

header name 的大小寫看起來怪怪的,Access-control-allow-origin 而不是 Access-Control-Allow-Origin,這正是前面說的 JDK Headers 正規化,它對送出去的 response header 也做同一件事,HTTP header name 不分大小寫,瀏覽器照樣認得,不影響任何行為,只是你自己在 curl 看到的時候不要覺得怪

Preflight

curl -i -X OPTIONS \
  -H "Origin: http://localhost:5173" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: Authorization, Content-Type" \
  localhost:8080/api/data
HTTP/1.1 204 No Content
Access-control-allow-origin: http://localhost:5173
Access-control-allow-methods: GET, POST, PUT, DELETE, PATCH, OPTIONS
Access-control-allow-headers: Content-Type, Authorization
Access-control-max-age: 86400
Vary: Origin

這筆 OPTIONS 根本沒有進到 app.routing { },CORS middleware 直接短路回 204,路由那邊也沒有註冊 OPTIONS /api/data

不允許的 origin

curl -i -H "Origin: http://evil.com" localhost:8080/api/data
HTTP/1.1 200 OK
Content-type: text/plain; charset=utf-8
Vary: Origin
Content-length: 4

data

這裡要注意,data 還是回來了,狀態碼也還是 200,差別只在少了 Access-Control-Allow-Origin 這個 header

CORS 從頭到尾都是瀏覽器端的機制,server 只負責在 response 上表態「我允許誰」,真正動手擋的是瀏覽器,curl 不管同源政策,所以你在 curl 看到的永遠是完整的 response

這也是為什麼 CORS 擋不住 curl、Postman 或任何後端對後端的呼叫,它不是安全機制,要保護 API 得靠認證那一層,第 23 篇會做

server 那邊的 terminal 三筆都記得到

[Relix] GET /api/data -> 200 (1ms)
[Relix] OPTIONS /api/data -> 204 (0ms)
[Relix] GET /api/data -> 200 (0ms)

第二筆是被 CORS 短路的 preflight,logging 一樣記得到,因為它裝在 CORS 外面

換成瀏覽器再看一次

想看瀏覽器真的擋下來,得開一個分頁

這裡有個容易搞錯的前提,瀏覽器送出的 Origin 是「你正開著的那個網頁」的 origin,不是你能在 JavaScript 裡指定的欄位,所以不能隨便找個分頁開 console 就打,得先讓自己站在一個不被允許的 origin 上

最省事的做法是隨手開一個靜態站,目錄不重要

python3 -m http.server 5500

瀏覽器打開 http://localhost:5500,開 DevTools 的 console,貼這段進去

// A:不允許的 origin,跟上面第三個 curl 情境一樣
try {
  const res = await fetch('http://localhost:8080/api/data')
  console.log('拿到了', res.status, await res.text())
} catch (e) {
  console.log('被擋掉:', e.message)
}

// B:同一支 request,改用 no-cors
const opaque = await fetch('http://localhost:8080/api/data', { mode: 'no-cors' })
console.log('opaque response → status =', opaque.status, ', body =', await opaque.text())

A,錯誤訊息把答案寫在同一行

Access to fetch at 'http://localhost:8080/api/data' from origin 'http://localhost:5500'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present
on the requested resource.

GET http://localhost:8080/api/data net::ERR_FAILED 200 (OK)

被擋掉: Failed to fetch

第二行的 net::ERR_FAILED200 (OK) 出現在同一行,傳輸層明明拿到了完整的 200 回應,但 CORS 檢查沒過,所以整個 fetch 被判定失敗,request 有送到、handler 有跑、data 有回來,是瀏覽器在最後一關才把它擋下來

第三行則是程式那邊看到的東西,e.message 只有一句 Failed to fetch。紅字裡那段講得很清楚的原因,JavaScript 完全讀不到,這是規格刻意的設計,避免前端靠錯誤訊息去試探別人家的 server,所以線上遇到 CORS 問題,你的錯誤回報系統通常只會收到 Failed to fetch,真正的原因只有打開 console 的人看得見

B,opaque response

opaque response → status = 0 , body =

no-cors 不會噴錯,但你拿到的是一個 opaque response,status 永遠是 0、body 永遠是空的

這時候回頭看 server 的 terminal,兩筆都在,而且都是 200

[Relix] GET /api/data -> 200 (0ms)
[Relix] GET /api/data -> 200 (0ms)

換成白名單裡的 origin 對照

把靜態站的 port 換成 CorsConfig 允許的那個

python3 -m http.server 5173

http://localhost:5173 再跑一次,A 會印出 拿到了 200 data,同一段 JavaScript、同一個 server,差別只在瀏覽器分頁的 origin

但 B 印出來的還是 status = 0

四種組合排在一起就清楚了

網頁 origin mode JavaScript 拿到 server 收到
5500 預設 拋錯,Failed to fetch 200
5500 no-cors opaque,status 0 200
5173 預設 200 data 200
5173 no-cors opaque,status 0 200

server 那一欄從頭到尾都是 200,四次都一樣,差別全部發生在瀏覽器把結果交給 JavaScript 之前那一刻

no-cors 不是繞過 CORS

最後一列的 origin 明明在白名單裡,no-cors 還是回 opaque

因為 mode: 'no-cors' 的意思不是「繞過 CORS 限制」,而是「我放棄讀這個 response」,你一開口就聲明不需要讀取結果,瀏覽器自然不必幫你做 CORS 檢查,也就不會噴錯,代價是永遠給你一個 opaque response,跟 server 有沒有回 Access-Control-Allow-Origin 無關

它真正的用途是那種送出去就好,不看結果的請求,例如 beacon、預熱快取,或是 <img><script> 這類本來就以 no-cors 模式發出的載入,拿它來要資料只會換另外一種卡法,錯誤訊息從紅字變成一個空的 response,會變的更難查

常見陷阱與設計取捨

測試全部通過,curl 卻回 405 ?

六個測試都是綠的,跑 main 用 curl 打一次 preflight,拿到的卻是

HTTP/1.1 405 Method Not Allowed
Allow: GET, HEAD, POST
Access-control-allow-origin: http://localhost:5173
Vary: Origin

原因就是前面講的大小寫,假設 isPreflight 當初寫成 request.headers.containsKey("Access-Control-Request-Method"),經過 JDK adapter 的 request 永遠不會成立,middleware 判斷這不是 preflight,就往 next() 送,router 發現 /api/data 只註冊了 GET 和 POST,回一個第 08 篇做的 405 加 Allow header,最後 CORS 再把自己的 header 補上去,才會看到這種「有 CORS header、狀態碼卻是 405」的奇怪組合

Origin 那條查得到,讓這個問題更難認出來,你會看到 Access-Control-Allow-Origin 好端端地掛在 response 上,很容易以為 CORS 已經通了

測試抓不到,是因為 RelixTestKit 建 request 的時候,是把你給的 headers 原封不動放進去 (第 06 篇建立、第 14 篇改成 suspend 版),沒有模擬 adapter 那一層的正規化,測試裡的 key 就一直維持標準大小寫,這是 test double 跟真實環境行為不一致的典型情況,測試寫得再多,也蓋不到那條你沒有模擬出來的差異

兩種補法,一種是像這篇一樣讓所有查詢都走 header(),另一種是讓 TestKit 也做同一份正規化,讓測試環境跟 adapter 對齊,這篇選前者,因為 header() 後面幾篇本來就要用,但如果你希望測試更貼近真實,後者也值得補上去

preflight 判斷只看 OPTIONS 夠嗎 ?

不夠,普通的 OPTIONS request (不帶 Access-Control-Request-Method header) 不是 preflight,可能是 client 在探測支援的 method,我們的 isPreflight 會同時檢查 Access-Control-Request-Method header,這樣就不會把一般的 OPTIONS 誤判成 preflight

disallowed origin 該回 403 還是正常回應 ?

兩種做法都有人用,回 403 比較明確 (server 拒絕了你),但正常回應然後不加 CORS header 也行,瀏覽器看到沒有 Access-Control-Allow-Origin 就會自己擋,我們選後者,因為比較簡單,而且 server-to-server 的 request 不需要 CORS header 也能正常運作

有個副作用要先知道,isOriginAllowed 的判斷排在 isPreflight 前面,所以不被允許的 origin 送 preflight 時不會短路回 204,而是往 next() 走,路由沒註冊 OPTIONS 就回 405,對瀏覽器來說結果一樣 (少了 Access-Control-Allow-Origin,preflight 就是失敗),但你在 log 看到 405 時要分得出來這是預期行為,不是前面那個大小寫問題又跑出來

Vary: Origin 要不要加 ?

如果 allowedOrigins 不是 *,response 的 CORS header 會因 Origin 不同而改變,CDN 或 proxy 可能誤用快取,所以上面的實作在白名單模式就把 Vary: Origin 加上去了

要注意的是被拒絕的那條路徑也要加,很容易只在「有回 Access-Control-Allow-Origin」的分支補 Vary,但沒有 ACAO 的那份回應同樣是看 Origin 決定出來的,一旦被快取下來,之後白名單裡的 origin 就可能拿到它,然後你會看到一個怎麼查都查不出原因的 CORS 錯誤


小結

CORS middleware 做兩件事,正常跨域 request 補 Access-Control-Allow-Origin,preflight request 短路回 204 並帶齊允許的 methods 和 headers,CorsConfig 用 data class 讓使用者可以設定白名單,安裝順序要在 auth 前面,不然 preflight 會被擋。中途還補了 header() 這個不分大小寫的查詢入口,JDK 會改 header name 的大小寫,preflight 判斷踩到就會回 405 而不是 204,wildcard 模式方便但跟 credentials 衝突,生產環境建議明確列出 origin


下一篇

下一篇把 Logging、Error Handling、CORS 這三個 middleware 提升為 Plugin,定義 RelixPlugin 介面,提供對外的安裝 API,讓框架更接近成熟框架的使用體驗


參考資料


同步刊登於 Blog

圖片來源:AI 產生


上一篇
Kotlin 手刻 Ktor 從零開始 Day 16 內建 Middleware,Error Handling
下一篇
Kotlin 手刻 Ktor 從零開始 Day 18 Plugin 系統,讓框架可擴展
系列文
Kotlin 手刻 Ktor 從零開始18
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言